package com.atguigu.redis7.double_write_consistency;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.atguigu.redis7.double_write_consistency.RedisConstants.CACHE_NULL_TTL;
import static com.atguigu.redis7.double_write_consistency.RedisConstants.LOCK_SHOP_KEY;


@Component
public class CacheClient {
    private final StringRedisTemplate stringRedisTemplate;

//    @Autowired
//    private CacheChannel channel;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /*
     * 不提供无参构造，要用的话必须存进一个StringRedisTemplate
     * */
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /*
     * 传入对象，帮忙序列化之后存redis
     * */
    public void set(String key, Object value, Long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
    }

    /*
     * 传入对象，在对象上添加逻辑过期时间（now+延迟），帮忙序列化之后存redis
     * */
    public void setWithLogicExpire(String key, Object value, Long time, TimeUnit timeUnit) {
        RedisData<Object> redisData = new RedisData<>();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
        set(key, redisData, time, timeUnit);
    }

    /*
     * 解决缓存穿透问题--null存进redis
     * */
    public <R, ID> R queryWithRassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> function, Long time, TimeUnit timeUnit) {
        //1、拼接key
        String key = keyPrefix + String.valueOf(id);

        //2、在redis中查询数据
        String json = stringRedisTemplate.opsForValue().get(key);

        //3、判断是否有数据
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }

        //4、如果查出来的是“”，即有对象，但是内容是“”，则是我们存的null
        if (json != null) {
            return null;
        }

        //5、调用数据库查询方法
        R r = function.apply(id);

        //6、如果查到是null
        if (r == null) {
            //7、null也存进去
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        //8、查到的不是null
        String jsonStr = JSONUtil.toJsonStr(r);

        //9、存进redis中
        this.set(key, jsonStr, time, timeUnit);

        //10、返回结果
        return r;
    }

    /*
     * 解决缓存缓存雪崩--加个随机时间
     * */
    public <R, ID> R queryWithCacheAvalanche(String keyPrefix, ID id, Class<R> type, Function<ID, R> function, Long time, TimeUnit timeUnit) {
        //1、拼接key
        String key = keyPrefix + String.valueOf(id);

        //2、在redis中查询数据
        String json = stringRedisTemplate.opsForValue().get(key);

        //整合ehcache 作为一级缓存
//        CacheObject cacheObject = channel.get(keyPrefix, (String) id);
//        String json = (String) cacheObject.getValue();

        //3、判断是否有数据
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }

        //4、如果查出来的是“”，即有对象，但是内容是“”，则是我们存的null
        if (json != null) {
            return null;
        }

        //5、调用数据库查询方法
        R r = function.apply(id);

        //6、如果查到是null
        if (r == null) {
            //7、null也存进去
//            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL+ RandomUtil.randomInt(-10, 10), TimeUnit.MINUTES);
            return null;
        }

        //8、查到的不是null
        String jsonStr = JSONUtil.toJsonStr(r);

        //9、存进redis中
        this.set(key,jsonStr,time+ RandomUtil.randomInt(-10, 10),timeUnit);
//        channel.set(keyPrefix, (String) id, jsonStr);

        //10、返回结果
        return r;
    }

    /*
     * 解决缓存击穿问题--逻辑删除
     * */
    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit) {
        //1. 从redis中查询商铺缓存
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);

        //2. 如果未命中，则返回空
        if (StrUtil.isBlank(json)) {
            return null;
        }

        //3. 命中，将json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();

        //4. 判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //5. 未过期，直接返回商铺信息
            return r;
        }

        //6. 过期，尝试获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean flag = tryLock(lockKey);
        //7. 获取到了锁
        if (flag) {
            //8. 开启独立线程
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    R tmp = dbFallback.apply(id);
                    this.setWithLogicExpire(key, tmp, time, timeUnit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unlock(lockKey);
                }
            });
            //9. 直接返回商铺信息
            return r;
        }
        //10. 未获取到锁，直接返回商铺信息
        return r;
    }

    /*
     * 解决缓存击穿问题--互斥锁
     * */
    public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit) {
        //1、先从Redis中查，这里的常量值是固定的前缀 + 店铺id
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);

        //2、如果不为空（查询到了）
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }

        //3、这里也处理了缓存穿透 --这里没必要这里处理，可以搞一下布隆过滤器
        if (json != null) {
            return null;
        }
        R r = null;
        String lockKey = LOCK_SHOP_KEY + id; //锁的key
        try {
            //4、否则去数据库中查
            boolean flag = tryLock(lockKey);

            //5.拿不到锁
            while (!flag) {
                Thread.sleep(50); //休息一下再去申请锁
                flag = tryLock(lockKey);
            }

            //6、这里拿到锁的话，再查一次redis  --双检锁
            json = stringRedisTemplate.opsForValue().get(key);

            //7、如果不为空（查询到了）
            if (StrUtil.isNotBlank(json)) {
                return JSONUtil.toBean(json, type);
            }

            //8、空值  没有长度的  相当于再去缓存雪崩存进去，这里也可以不用的
            if (json != null) {
                return null;
            }

            //方法引用  apply调用
            r = dbFallback.apply(id);

            //9、查不到，则将空值写入Redis  空值2分钟
            if (r == null) {
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }

            //10、查到也存入redis，设置TTL
            this.set(key, r, time, timeUnit);

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //11、释放锁
            unlock(lockKey);
        }
        return r;
    }

    /*
     * 尝试获得锁
     * */
    private boolean tryLock(String key) {
        //设置，如果不存在的话
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /*
     * 释放锁
     * */
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}
